package com.google.code.stalkernet.service;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.code.stalkernet.advertisement.Advertisement;
import com.google.code.stalkernet.advertisement.AdvertisementUtils;
import com.google.code.stalkernet.advertisements.Advertisements;
import com.google.code.stalkernet.node.Node;
import com.google.code.stalkernet.node.NodeID;
import com.google.code.stalkernet.server.Server;
import com.google.code.stalkernet.server.event.ServerEvent;
import com.google.code.stalkernet.server.event.ServerListener;

/**
 * Service that automatically advertises server socket addresses.
 *
 * @author Mikhail Vladimirov
 */
public class ServerAdvertiserService implements Service
{
    private final static Logger logger = Logger.getLogger (
        ServerAdvertiserService.class.getName ());

    private final MyServerListener myServerListener = new MyServerListener ();

    private final Server server;
    private final Advertisements advertisements;
    private final NodeID nodeID;

    private final Map <SocketAddress, Collection <Advertisement>>
        myAdvertisements = new HashMap <> ();

    /**
     * Create new server advertiser service that advertises socket addresses of
     * given server with given node.
     *
     * @param server {@link Server} to advertise socket addresses of
     * @param node {@link Node} to advertise with
     */
    public ServerAdvertiserService (
        Server server, Advertisements advertisements, NodeID nodeID)
    {
        if (server == null)
            throw new IllegalArgumentException ("Server is null");

        if (advertisements == null)
            throw new IllegalArgumentException ("Advertisements is null");

        if (nodeID == null)
            throw new IllegalArgumentException ("Node ID is null");

        this.server = server;
        this.advertisements = advertisements;
        this.nodeID = nodeID;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void start ()
    {
        for (SocketAddress socketAddress: server.getAllSocketAddresses ())
            advertiseSocketAddress (socketAddress);

        server.addServerListener (myServerListener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stop ()
    {
        server.removeServerListener (myServerListener);

        for (Collection <Advertisement> advertisements:
            this.myAdvertisements.values ())
        {
            for (Advertisement advertisement: advertisements)
                this.advertisements.removeAdvertisement (advertisement);
        }
    }

    private void advertiseSocketAddress (SocketAddress socketAddress)
    {
        if (socketAddress == null)
            throw new IllegalArgumentException ("Socket address is null");

        if (socketAddress instanceof InetSocketAddress)
        {
            InetSocketAddress inetSocketAddress =
                (InetSocketAddress)socketAddress;

            Collection <Advertisement> advertisements = new ArrayList <> ();
            if (inetSocketAddress.getAddress ().isAnyLocalAddress ())
            {
                int port = inetSocketAddress.getPort ();

                try
                {
                    Enumeration <NetworkInterface> networkInterfaces =
                        NetworkInterface.getNetworkInterfaces ();
                    while (networkInterfaces.hasMoreElements ())
                    {
                        NetworkInterface networkInterface =
                            networkInterfaces.nextElement ();

                        Enumeration <InetAddress> inetAddresses =
                            networkInterface.getInetAddresses ();
                        while (inetAddresses.hasMoreElements ())
                            advertisements.add (
                                AdvertisementUtils.createAdvertisement (
                                    new InetSocketAddress (
                                        inetAddresses.nextElement (),
                                        port),
                                    nodeID));
                    }
                }
                catch (SocketException ex)
                {
                    logger.log (
                        Level.SEVERE, "Can not get network interfaces", ex);
                }
            }
            else advertisements.add (
                AdvertisementUtils.createAdvertisement (
                    inetSocketAddress, nodeID));

            myAdvertisements.put (socketAddress, advertisements);

            for (Advertisement advertisement: advertisements)
                this.advertisements.addAdvertisement (advertisement);
        }
    }

    private class MyServerListener implements ServerListener
    {
        @Override
        public void socketAddressAdded (ServerEvent serverEvent)
        {
            if (serverEvent == null)
                throw new IllegalArgumentException ("Server event is null");

            advertiseSocketAddress (serverEvent.getSocketAddress ());
        }

        @Override
        public void socketAddressRemoved (ServerEvent serverEvent)
        {
            if (serverEvent == null)
                throw new IllegalArgumentException ("Server event is null");

            SocketAddress socketAddress = serverEvent.getSocketAddress ();
            Collection <Advertisement> advertisements =
                ServerAdvertiserService.this.myAdvertisements.remove (
                    socketAddress);
            if (advertisements != null)
            {
                for (Advertisement advertisement: advertisements)
                   ServerAdvertiserService.this.advertisements.
                       removeAdvertisement (advertisement);
            }
        }
    }
}
